/*
 * Copyright (C) Francisco Morero Peyrona. All rights reserved.
 *
 * This software is published under the terms of Open Source
 * License version 1.1, a copy of which has been included with 
 * this distribution in the License.txt file.
 */

package jdbcmanager.table;

import java.sql.*;

import java.awt.event.MouseEvent;

import javax.swing.JMenuItem;
import javax.swing.event.*;

import jdbcmanager.swing.table.*;
import jdbcmanager.app.*;
import jdbcmanager.dbtools.*;

import com.sun.rowset.*;
 
/**
 * Creates a table (by inheriting from <code>JxTable</code>) showing the ResultSet (if any) obtained by executing passed
 * <code>sSQL</code> statement againts passed connection.
 * 
 * @author Francisco Morero Peyrona
 */
final class DataGrid extends JxTable
{
	private Connection     conn   = null;
	private JdbcRowSetImpl rowset = null;
	
	private boolean bUpdateableState = false;  // Is in updateable state (green locker icon)?
	private boolean bCanBeUpdateable = false;  // Can physically be under updateable state?

	//--------------------------------------------------------------------//

	/**
	 * Class constructor
	 */
	DataGrid( Connection conn )
	{
		this.conn = conn;
	}

	/**
	 * Neede to be redefined, see: 
	 * http://java.sun.com/docs/books/tutorial/uiswing/components/tooltip.html
	 */
	public String getToolTipText( MouseEvent evt )
    {
        int nCol = columnAtPoint( evt.getPoint() );

        return ((GridTableModel) getModel()).createTooltip( nCol );
    }

	void execute( String sSQL, boolean bUpdateable, int nMaxRows )
	{
		this.bUpdateableState = bUpdateable;

		try
		{
			createData( sSQL, nMaxRows );
		}
		catch( Exception exc )
		{
			App.showException( exc );
		}
	}

	/**
     * Return "Print" item in popup menu (defined in JxTable)
	 * @return  "Print" item in popup menu (defined in JxTable)
	 */
    JMenuItem getPrintPopupMenuItem()
    {
        return popupMenu.getItem( "PRINT" );
    }

	/**
     * Return "Export" item in popup menu (defined in JxTable). <br />
     * This is useful to add actionListeners to the popup menu items.
	 * @return  "Export" item in popup menu (defined in JxTable)
	 */
    JMenuItem getExportPopupMenuItem()
    {
        return popupMenu.getItem( "EXPORT" );
    }    
    
	ResultSetMetaData getMetaData()
	{
		ResultSetMetaData rsmd = null;

		try
		{
			if( this.rowset != null )
				rsmd = this.rowset.getMetaData();
		}
		catch( SQLException exc )
		{
			App.showException(exc);
		}

		return rsmd;
	}

	boolean canBeUpdateable()
	{
		return this.bCanBeUpdateable;
	}

	boolean isUpdateable()
	{
		return this.bUpdateableState;
	}

	void setUpdateable( boolean b )
	{
		if( this.bCanBeUpdateable )
		{
			if( this.bUpdateableState && (! b) )
				editingCanceled( new ChangeEvent( this ) );

			this.bUpdateableState = b;
			((GridTableModel) getModel()).setEditable( b );
		}
	}

	void first()
	{
		if( getRowCount() > 0 )
		{
			setRowSelectionInterval( 0, 0 );
			scrollRectToVisible(getCellRect( 0, 0, true ) );
		}
	}

	void last()
	{
		int nRows = getRowCount() - 1;

		if( nRows >= 0 )
		{
			setRowSelectionInterval( nRows, nRows );
			scrollRectToVisible( getCellRect( nRows, 0, true ) );
		}
	}

	void previous()
	{
		int nRow = getSelectedRow() - 1;

		if( nRow >= 0 )
		{
			setRowSelectionInterval( nRow, nRow );
			scrollRectToVisible( getCellRect( nRow, 0, true ) );
		}
	}

	void next()
	{
		int nRow = getSelectedRow() + 1;

		if( nRow < getRowCount() )
		{
			setRowSelectionInterval( nRow, nRow );
			scrollRectToVisible( getCellRect( nRow, 0, true ) );
		}
	}

	void addRow()
	{
		try
		{
			this.rowset.moveToInsertRow();
			this.rowset.insertRow();
			this.rowset.moveToCurrentRow();
			((GridTableModel) getModel()).rowAdded( this.rowset.getRow() );
		}
		catch( SQLException exc )
		{
			App.showException( exc, "Error adding a new record" );
		}
	}

	void delRow()
	{
		try
		{
			this.rowset.deleteRow();
			((GridTableModel) getModel()).rowDeleted( this.rowset.getRow() );
		}
		catch( SQLException exc )
		{
			App.showException( exc, "Error deleting highlighted record" );
		}
	}

	//-------------------------------------------------------------------------//

	// Next time this method is called, old "stmt" and "rs" vars will losse their references and
	// the g.c. will invoke its finalize() method. (I don't see any better way to perform the "cleaning")
	private void createData( String sSQL, int nMaxRows ) throws SQLException, InterruptedException
	{
		// It is not be necessary to create an Statement each time, but in this way I ensure more compatibility through all drivers
		Statement stmt = conn.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE );
		          stmt.setQueryTimeout( App.props().getConnQueryTimeOut( 0 ) );
		          stmt.setMaxRows( nMaxRows );
		ResultSet rs = DbUtil.executeSQL( stmt, sSQL, false );

		// rs == null --> sSQL is not a SELECT (can be an "UPDATE", "DELETE", etc.)
		// Re-query is needed to reflect possible changes
		if( rs == null && this.rowset != null && this.rowset.getCommand() != null )
		{
			sSQL = this.rowset.getCommand();
			rs = DbUtil.executeSQL( stmt, sSQL, false );
		}

		// At this point, rs should not be null
		if( rs != null )
		{
			boolean bScrollable = checkScrollable( rs );
			
			// Checks if in fact the rs can be updatable and updates Updateable state based on bCanBeUpdateable
			this.bCanBeUpdateable = bScrollable && checkUpdateable( rs );
			this.bUpdateableState = this.bUpdateableState && bCanBeUpdateable;
			
			this.rowset = new MyJdbcRowSetImpl( (bScrollable) ? rs : new RowSetRam( rs ) );
			this.rowset.setType( ResultSet.TYPE_SCROLL_INSENSITIVE );
			this.rowset.setConcurrency( ResultSet.CONCUR_UPDATABLE );
			this.rowset.setTransactionIsolation( Connection.TRANSACTION_READ_COMMITTED );
			this.rowset.setReadOnly( false );
			this.rowset.setCommand( sSQL );
			this.rowset.setMaxRows( nMaxRows );
			this.rowset.absolute( 1 );

			ResultSetMetaData rsmd = getMetaData();
			GridTableModel    gtm  = new GridTableModel( this.rowset, rsmd );
			                  gtm.setEditable( this.bUpdateableState );
			                  gtm.addTableModelListener( new TableModelListener()
		                  		{
									public void tableChanged( TableModelEvent evt )
									{
										if( evt.getType() == TableModelEvent.UPDATE )
											try
											{
                                                createData( rowset.getCommand(), rowset.getMaxRows() );
											}
											catch( Exception exc )
											{
												exc.printStackTrace();
											}
									}
		                  		} );
			setModel( gtm );

			// Adjusts every column to fit its data size ---------------
			for( int n = 0; n < getColumnModel().getColumnCount(); n++ )
				setColumnWidth( n, rsmd.getColumnDisplaySize( n + 1 ) );

			if( getRowCount() > 0 )
				setRowSelectionInterval( 0, 0 );

			lstRowNumberer.repaint(); // lstRowNumberer -> defined in JxTable
		}
	}

	// The logical way to find if a ResultSet is Scrollable is by doing:
	// return res.getType() == ResultSet.TYPE_SCROLL_INSENSITIVE.
	// But the fact is that some drivers do not return the type properly.
	// The workaround I choosen is to test if the "scrolling" methods that are needed work.
	private boolean checkScrollable( ResultSet rs )
	{
		boolean bScrollable = false;

		try
		{
			bScrollable = rs.getType() != ResultSet.TYPE_FORWARD_ONLY;

			if( bScrollable ) // If rs says it is scrollable, lets check it
			{
				if( rs.last() )
				{
					rs.first();
					rs.absolute( rs.getRow() );
                    rs.beforeFirst();            // Needed
				}
			}
		}
		catch( SQLException exc )
		{
            try   // Some scrollable ResultSets allow rs.first(), but do not allow rs.absolute(n).
            {     // Therefore, without the next the 1st record would not be part of RowSetRam. 
                rs.beforeFirst();
            }
            catch( SQLException e )
            {
                // Nothing to do
            }
            
			bScrollable = false; // It is proven that is not scrollable
		}

		return bScrollable;
	}

	// The logical way to find if a ResultSet is Updatable is by doing:
	// res.getConcurrency() == ResultSet.CONCUR_UPDATABLE;
	// But in a ResultSet some of its columns can be updatebale and other not;
	// therefore, this method returns true if at least one of the columns is updateable,
	// and ThisTableModel.isCellEditable() returns if a concrete columns is updateable.
	private boolean checkUpdateable( ResultSet res )
	{
		try
		{
			ResultSetMetaData rsmd  = res.getMetaData();
			int               nCols = rsmd.getColumnCount();

			for( int n = 0; n < nCols; n++ )
				if( ! rsmd.isReadOnly( n + 1 ) )
					return true;                            // return here !! 
		}
		catch( SQLException e )
		{
			// Nothing to do
		}

		return false;
	}
    
    
    //------------------------------------------------------------------------//
    // INNER CLASS: Dirty trick to solve something like a bug in Sun's 
    //              implementation of JdbcRowSetImpl.
    // Some implementations of ResulSetMetaData (like PostgreSQL or JTDS JDBC 
    // drivers) return for certain columns a negative display size number when
    // rsmd.getColumnDisplaySize( <n> ) is invoked. This produces an exception.
    // The same thing happen with Oracle and rsmd.setScale(...) method.
    // As I supect that other DBMSs can have the same problem with setPrecsion()
    // I changed it to prevent possible problems.
	//------------------------------------------------------------------------//
    
    private class MyJdbcRowSetImpl extends JdbcRowSetImpl
    {
        private MyJdbcRowSetImpl( ResultSet rs ) throws SQLException
        {
            super( rs );
        }
        
        protected void initMetaData( javax.sql.RowSetMetaData md, ResultSetMetaData rsmd ) throws SQLException
        {
            int numCols = rsmd.getColumnCount();

            md.setColumnCount( numCols );
            
            for( int nCol = 1; nCol <= numCols; nCol++ )
            {
                boolean b = false;
                try { b = rsmd.isAutoIncrement( nCol ); } catch( Exception e ){}
                md.setAutoIncrement( nCol, b );
                
                b = false;
                try { b = rsmd.isCaseSensitive( nCol ); } catch( Exception e ){}
                md.setCaseSensitive( nCol, b );
                
                b = false;
                try { b = rsmd.isCurrency( nCol ); } catch( Exception e ){}
                md.setCurrency( nCol, b );
                
                int n = ResultSetMetaData.columnNullableUnknown;
                try { n = rsmd.isNullable( nCol ); } catch( Exception e ){}
                md.setNullable( nCol, n );
                
                b = false;
                try { b = rsmd.isSigned( nCol ); } catch( Exception e ){}
                md.setSigned( nCol, b );
                
                b = false;
                try { b = rsmd.isSearchable( nCol ); } catch( Exception e ){}
                md.setSearchable( nCol, b );
                
                String sColName = "UnknownColumnName";
                try { sColName = rsmd.getColumnName( nCol ); } catch( Exception e ){}
                md.setColumnName( nCol, sColName );
                
                String sColLabel = sColName;
                try { sColLabel = rsmd.getColumnLabel( nCol ); } catch( Exception e ){}
                md.setColumnLabel( nCol, sColLabel );
                
                String sSchema = null;
                try { sSchema = rsmd.getSchemaName( nCol ); } catch( Exception e ){}
                md.setSchemaName( nCol, sSchema );
                
                String sTableName = null;
                try { sTableName = rsmd.getTableName( nCol ); } catch( Exception e ){}
                md.setTableName( nCol, sTableName );
                
                String sCatalogName = null;
                try { sCatalogName = rsmd.getCatalogName( nCol ); } catch( Exception e ){}
                md.setCatalogName( nCol, sCatalogName );
                
                String sColumnTypeName = null;
                try { sColumnTypeName = rsmd.getColumnTypeName( nCol ); } catch( Exception e ){}
                md.setColumnTypeName( nCol, sColumnTypeName );
                
                long nSize = -1;
                try { nSize = rsmd.getColumnDisplaySize( nCol ); } catch( Exception e ){}
                md.setColumnDisplaySize( nCol, (nSize < Integer.MAX_VALUE && nSize > 0) ? (int)nSize : 9 );
                
                long nScale = -1;
                try { nScale = rsmd.getScale( nCol ); } catch( Exception e ){}
                md.setScale( nCol, Math.max( (int)nScale, 0 ) );
                
                // We have to convert the column type to one of the standards TODO: check what happens with name
                int nColType = Types.OTHER;
                try { nColType = rsmd.getColumnType( nCol ); } catch( Exception e ){}
                md.setColumnType( nCol, DbUtil.getAgentDBMS().getAgentMetaData().mapType( nColType ) );
                
                // Set precision: if it fits into an int and is not a negative number, otherwise set to zero
                long nPrec = -1;
                try { nPrec = rsmd.getPrecision( nCol ); } catch( Exception e ){}
                md.setPrecision( nCol, (nPrec < Integer.MAX_VALUE && nPrec > 0) ? (int)nPrec : 0 );
            }
        }
    }
}